package com.djx.comm.util;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.crypto.SmUtil;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.SM2;
import cn.hutool.crypto.asymmetric.SM2Engine;
import com.djx.comm.exception.DecryptedException;
import com.djx.comm.exception.RepeatEncryptException;
import com.djx.comm.exception.UnNecessaryDecryptException;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.pqc.math.linearalgebra.ByteUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;

import javax.crypto.*;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Base64;

/**
 * 电子文件加解密类
 *
 * @author dingjunxiong
 * @date 2022-04-03
 */
public class CrcSecurityTool {

    private static final Logger logger = LoggerFactory.getLogger(CrcSecurityTool.class);
    private static final int KEY_SIZE = 128;
    private static final String ALGORITHM_NAME = "SM4";
    private static final String PBK_SHA1 = "PBKDF2WithHmacSHA1";

    /**
     * 构造函数
     */
    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    private CrcSecurityTool() {
    }


    /**
     * 生成动态秘钥
     *
     * @return
     * @throws Exception
     */
    public static String generateKey() {
        String key = "";
        try {
            KeyGenerator kg = KeyGenerator.getInstance(ALGORITHM_NAME, BouncyCastleProvider.PROVIDER_NAME);
            kg.init(KEY_SIZE, new SecureRandom());
            key = ByteUtils.toHexString(kg.generateKey().getEncoded());
        } catch (NoSuchAlgorithmException e) {
            logger.error("生产sm4密钥失败", e);
            throw new RuntimeException("SecTools:generateKey:" + e.getMessage(), e);
        } catch (NoSuchProviderException e) {
            logger.error("生产sm4密钥失败", e);
            throw new RuntimeException("SecTools:generateKey:" + e.getMessage(), e);
        }
        return key;
    }

    /**
     * 生成 Cipher
     *
     * @param mode
     * @param keyData
     * @return
     * @throws Exception
     */
    private static Cipher generateCipher(int mode, byte[] keyData) {
        try {
            Cipher cipher = Cipher.getInstance("SM4/ECB/PKCS5Padding", BouncyCastleProvider.PROVIDER_NAME);
            Key sm4Key = new SecretKeySpec(keyData, ALGORITHM_NAME);
            cipher.init(mode, sm4Key);
            return cipher;
        } catch (NoSuchPaddingException e) {
            logger.error("generateCipher方法执行失败", e);
            throw new RuntimeException("SecTools:generateCipher:" + e.getMessage(), e);
        } catch (NoSuchAlgorithmException e) {
            logger.error("generateCipher方法执行失败", e);
            throw new RuntimeException("SecTools:generateCipher:" + e.getMessage(), e);
        } catch (NoSuchProviderException e) {
            logger.error("generateCipher方法执行失败", e);
            throw new RuntimeException("SecTools:generateCipher:" + e.getMessage(), e);
        } catch (InvalidKeyException e) {
            logger.error("generateCipher方法执行失败", e);
            throw new RuntimeException("SecTools:generateCipher:" + e.getMessage(), e);
        }
    }

    /**
     * true: 加密过， false：未加密
     *
     * @param file
     * @return true: 加密过， false：未加密
     * @throws FileNotFoundException
     */
    public static boolean isEncrypted(File file) throws IOException {
        if (!file.exists()) {
            throw new FileNotFoundException("电子文件不存在！" + file.getPath());
        }
        // 小于8 byte没有加密标志位，肯定没加密
        if (file.length() < 8) {
            return false;
        }
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(file);
            fis.skip(file.length() - 8);
            byte[] b = new byte[8];
            fis.read(b);
            if ("AAAAAAAAAAAAAAAA".equals(bytesToHexString(b))) {
                return true;
            }
        } catch (IOException e) {
            logger.error("判断文件是否加密失败");
            throw e;
        } finally {
            IoUtil.close(fis);
        }
        return false;
    }

    /**
     * 加密电子文件
     *
     * @param secretKey  密钥
     * @param sourcePath 源文件路径
     * @throws Exception 异常信息
     */
    public static File encryptFile(String secretKey, String sourcePath) throws FileNotFoundException, IOException, RepeatEncryptException {
        //加密文件
        CipherInputStream cipherInputStream = null;
        FileInputStream fis = null;
        ByteArrayOutputStream bos = null;
        byte[] bufferPrifx = null;
        String hexStr = "";
        String targetPath = "";
        FileOutputStream fileOutputStream = null;
        File file = new File(sourcePath);
        if (!file.exists()) {
            throw new FileNotFoundException("电子文件不存在！" + sourcePath);
        }
        try {
            //读取文件流
            fis = new FileInputStream(file);
            bos = new ByteArrayOutputStream(8);
            // 电子文件大小8,说明没有加密标记位，肯定没有加密过
            if (file.length() > 8) {
                byte[] b = new byte[8];
                int n;
                int loopCnt = 1;
                fis.skip(file.length() - 8);
                while ((n = fis.read(b)) != -1 && loopCnt == 1) {
                    bos.write(b, 0, n);
                    loopCnt++;
                }
                fis.close();
                bos.close();
                bufferPrifx = bos.toByteArray();
                hexStr = bytesToHexString(bufferPrifx);
                if ("AAAAAAAAAAAAAAAA".equals(hexStr)) {
                    // 已经被加密过
                    throw new RepeatEncryptException();
                }
            }
            String fileName = file.getName();
            String fileNameJd = fileName.substring(0, fileName.lastIndexOf("."));
            String fileNameType = fileName.substring(fileName.lastIndexOf(".") + 1);
            targetPath = file.getParent() + File.separator + fileNameJd + " sm4" + "." + fileNameType;
            byte[] keyData = ByteUtils.fromHexString(secretKey);
            Cipher cipher = generateCipher(Cipher.ENCRYPT_MODE, keyData);
            cipherInputStream = new CipherInputStream(new FileInputStream(sourcePath), cipher);
            FileUtil.writeFromStream(cipherInputStream, targetPath);
            IoUtil.close(cipherInputStream);
//            file.delete();
            File fileNow = new File(targetPath);
            //将尾部追加8位标识符变成AAAAAAAAAAAAAAAA
            fileOutputStream = new FileOutputStream(fileNow, true);
            fileOutputStream.write(parseHexStr2Byte("AAAAAAAAAAAAAAAA"), 0, 8);
            IoUtil.close(fileOutputStream);
//            fileNow.renameTo(file);
            return fileNow;
        } catch (IOException e) {
            logger.error("加密失败");
            throw e;
        } finally {
            IoUtil.close(fileOutputStream);
            IoUtil.close(cipherInputStream);
            IoUtil.close(fis);
            IoUtil.close(bos);
            Files.delete(Paths.get(sourcePath));
        }
    }

    /**
     * 解密电子文件
     *
     * @param secretKey  解密密钥
     * @param sourcePath 源文件路径
     * @param targetPath 解密后文件路径
     * @throws UnNecessaryDecryptException 电子文件不需要解密的异常
     */
    public static void decryptFile(String secretKey, String sourcePath, String targetPath) throws FileNotFoundException, IOException, UnNecessaryDecryptException {
        if (sourcePath == null || targetPath == null || sourcePath.equals(targetPath)) {
            throw new RuntimeException("加/解密文件路径不能为空并且加密解密文件路径及名称不能一样");
        }
        FileUtil.touch(targetPath);
        InputStream is = null;
        OutputStream out = null;
        CipherOutputStream cos = null;
        File fileNow = new File(sourcePath);
        if (!fileNow.exists()) {
            throw new FileNotFoundException("电子文件不存在!" + sourcePath);
        }
        // 未加密不需要解密
        if (!CrcSecurityTool.isEncrypted(fileNow)) {
            throw new UnNecessaryDecryptException();
        }
        try {
            is = new FileInputStream(sourcePath);
            out = new FileOutputStream(targetPath);
            Cipher cipher = generateCipher(Cipher.DECRYPT_MODE, ByteUtils.fromHexString(secretKey));
            // 创建解密流
            cos = new CipherOutputStream(out, cipher);
            // 字节数组长度不能小于8.
            byte[] buffer = new byte[2048];
            int r = 0;
            // 计算字节数组中的加密标志位
            int flagByteCount = 0;
            long actualLength = 0L;
            long fileLength = fileNow.length();
            while (true) {
                r = is.read(buffer);
                // 最后一次读取buffer读不满的情况
                if (r < buffer.length) {
                    break;
                }
                actualLength = actualLength + r;
                // 最后一次读取buffer刚好读满的情况或者倒数第二次读取读到了加密标志位
                if (actualLength + 8 >= fileLength) {
                    flagByteCount = (int) (8 - (fileLength - actualLength));
                    break;
                }
                cos.write(buffer, 0, r);
            }
            if (r < buffer.length) {
                // 最后一次读取buffer读不满的情况
                byte[] endBytes = new byte[r - 8];
                System.arraycopy(buffer, 0, endBytes, 0, r - 8);
                cos.write(endBytes, 0, r - 8);
            } else if (flagByteCount >= 0) {
                // 最后一次读取buffer刚好读满的情况或者倒数第二次读取读到了加密标志位
                byte[] endBytes = new byte[buffer.length - flagByteCount];
                System.arraycopy(buffer, 0, endBytes, 0, buffer.length - flagByteCount);
                cos.write(endBytes, 0, buffer.length - flagByteCount);
            } else {
                // 不可达路径
                throw new DecryptedException();
            }
        } catch (IOException e) {
            logger.error("解密失败");
            throw e;
        } finally {
            IoUtil.close(cos);
            IoUtil.close(out);
            IoUtil.close(is);
        }
    }

    /**
     * 二进制流转16进制
     *
     * @param src
     * @return
     */
    public static String bytesToHexString(byte[] src) {
        StringBuilder builder = new StringBuilder();
        if (src == null || src.length <= 0) {
            return null;
        }
        String hv;
        for (int i = 0; i < src.length; i++) {
            // 以十六进制（基数 16）无符号整数形式返回一个整数参数的字符串表示形式，并转换为大写
            hv = Integer.toHexString(src[i] & 0xFF).toUpperCase();
            if (hv.length() < 2) {
                builder.append(0);
            }
            builder.append(hv);
        }
        return builder.toString();
    }

    /**
     * 16进制转二进制流
     *
     * @param hexStr
     * @return
     */
    public static byte[] parseHexStr2Byte(String hexStr) {
        if (hexStr.length() < 1) {
            throw new RuntimeException("parseHexStr2Byte方法入参不合法");
        }
        byte[] result = new byte[hexStr.length() / 2];
        for (int i = 0; i < hexStr.length() / 2; i++) {
            int high = Integer.parseInt(hexStr.substring(i * 2, i * 2 + 1), 16);
            int low = Integer.parseInt(hexStr.substring(i * 2 + 1, i * 2 + 2),
                    16);
            result[i] = (byte) (high * 16 + low);
        }
        return result;
    }


    public static void main(String[] args) throws InvalidKeySpecException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException {
        System.out.println(decryptText(encryptText("ce成第三发送","afeawredafe"),"afeawredafe"));
    }

    /**
     * 字符串加密
     *
     * @param data 字符串
     * @param key  秘钥
     * @return 加密后内容
     * @throws Exception 异常
     */
    public static String encryptText(String data, String key) throws NoSuchAlgorithmException, InvalidKeySpecException, BadPaddingException, IllegalBlockSizeException {
        try {
            if(StringUtils.isEmpty(data)){
                return "";
            }
            byte[] rawKey = getRawKey(key);
            Cipher cipher = generateCipher(Cipher.ENCRYPT_MODE, rawKey);
            byte[] bytes = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(bytes);
        } catch (NoSuchAlgorithmException | InvalidKeySpecException | BadPaddingException | IllegalBlockSizeException e) {
            logger.error("encryptText error , data:{}", data, e);
            throw e;
        }
    }

    /**
     * 字符串解密
     *
     * @param data 加密字符串
     * @param key  秘钥
     * @return 解密后内容
     * @throws Exception 异常
     */
    public static String decryptText(String data, String key) throws NoSuchAlgorithmException, InvalidKeySpecException, BadPaddingException, IllegalBlockSizeException {
        try {
            if(StringUtils.isEmpty(data)){
                return "";
            }
            byte[] rawKey = getRawKey(key);
            Cipher cipher = generateCipher(Cipher.DECRYPT_MODE, rawKey);
            byte[] bytes = cipher.doFinal(Base64.getDecoder().decode(data));
            return new String(bytes, StandardCharsets.UTF_8);
        } catch (NoSuchAlgorithmException | InvalidKeySpecException | BadPaddingException | IllegalBlockSizeException e) {
            logger.error("decryptText error , data:{}", data, e);
            throw e;
        }
    }

    /**
     * 使用一个安全的随机数来产生一个密匙,密匙加密使用的
     *
     * @param key 秘钥
     * @throws NoSuchAlgorithmException 模式错误
     */
    private static byte[] getRawKey(String key) throws NoSuchAlgorithmException, InvalidKeySpecException {
        int count = 1000;
        int keyLen = KEY_SIZE;
        int saltLen = keyLen / 8;
        byte[] salt = new byte[saltLen];
        KeySpec keySpec = new PBEKeySpec(key.toCharArray(), salt, count, keyLen);
        SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(PBK_SHA1);
        return secretKeyFactory.generateSecret(keySpec).getEncoded();
    }

    /**
     * 数字信封生成
     * @param crtPath 公钥证书路径
     * @param plainText 动态秘钥
     * @return 数字信封
     * @throws Exception 异常
     */
    public static String digitalLetterEncryption(String crtPath,String plainText) throws Exception {

        try (FileInputStream fin=new FileInputStream(crtPath)){

            String encryptStr="";
            //对sm2的支持
            Security.removeProvider("SunEC");

            CertificateFactory f = CertificateFactory.getInstance("X.509");

            X509Certificate certificate = (X509Certificate)f.generateCertificate(fin);

            //验证证书有效期
            certificate.checkValidity();

            //获取公钥
            PublicKey pk = certificate.getPublicKey();

            byte[] publicKey = pk.getEncoded();

            //通过公钥实例化sm2对象
            SM2 sm2 = SmUtil.sm2(null,publicKey);

            //设置公钥加密模式
            sm2.setMode(SM2Engine.SM2Mode.C1C2C3);

            // 公钥加密
            encryptStr = sm2.encryptBcd(plainText, KeyType.PublicKey);

            return  encryptStr;
        }
        catch (Exception ex)
        {
            logger.error("数字信封加密失败!"+crtPath+":"+ex.getMessage());
            throw  new Exception("数字信封加密失败!"+crtPath+":"+ex.getMessage());
        }
    }

}
